查看原文
其他

Kubelet从人门到放弃:拓扑管理(下)

zouyee DCOS 2022-07-13


友情提示:全文4090多文字,预计阅读时间6分钟

摘要

《Kubelet从入门到放弃系列》将对Kubelet组件由Linux基础知识到源码进行深入梳理。上一篇zouyee带各位看了Kubelet从入门到放弃:拓扑管理(上),其中介绍了拓扑管理,本文将对拓扑管理进行源码深入剖析。



一、源码分析


      注:代码逻辑对应版本1.21.0-beta.0

        对于拓扑管理器代码分析,我们从两个方面进行:

1)Kubelet初始化时,涉及拓扑管理的相关操作

2)Kubelet运行时,涉及拓扑管理的相关操作,深入分析拓扑管理结构逻辑

       1.1 Kubelet初始化      关于Kubelet初始化,我们将以CPU manager结合拓扑管理器的启动图(当前为CPU manager、memory manager、device manager构成资源分配管理器,其属于Container Manager模块的子系统)进行说明。


对于上图的内容,zouyee总结流程如下:

1、在命令行启动部分,Kubelet中调用NewContainerManager构建ContainerManager

2、NewContainerManager函数调用topologymanager.NewManager构建拓扑管理器,否则未启用拓扑管理器,则构建fakeManager

3、NewContainerManager函数分别调用cpu、memory及device提供的NewManager构建相关管理器

4、若拓扑管理特性开启,则拓扑管理器使用AddHintPriovider方法将CPU、memory及device管理器加入管理,上述三种资源分配器,需要实现HintPriovider接口

5、回到命令行启动部分,调用NewMainKubelet(),构建Kubelet结构体

6、构建Kubelet结构体时,将CPU、memory管理器(没有device)跟拓扑管理器封装为InternalContainerLifecycle接口,其实现Pod相关的生命周期资源管理操作,涉及资源分配回收相关的是PreStartContainer、PostStopContainer方法,可参看具体实现。

7、构建Kubelet结构体时,调用AddPodmitHandler将GetAllocateResourcesPodAdmitHandler方法加入到Pod准入插件中,在Pod创建时,资源预分配检查,其中GetAllocateResourcesPodAdmitHandler根据是否开启拓扑管理,决定是返回拓扑管理Admit接口,还是使用cpu、memory及device构成资源分配器,实现Admit接口。

8、构建Kubelet结构体后,调用ContainerManager的Start方法,ContainerManager在Start方法中调用CPU、memory及device管理器的Start方法,其做一些处理工作并孵化一个goroutine,执行reconcileState()

        注:关于上述启动流程的代码解释,可以返回Kubelet从入门到放弃:识透CPU管理


1.2 Kubelet运行时

Kubelet运行时,涉及到拓扑管理、资源分配的就是对于Pod处理流程,zouyee总结如下:

1、PodConfig从apiserver、file及http三处接受Pod,调用Updates()返回channel,内容为Pod列表及类型。

2、Kubelet调用Run方法,处理PodConfig的Updates()返回的channel

3、在Run方法内部,Kubelet调用syncLoop,而在syncLoop内部,调用syncLoopIteration

4、在syncLoopIteration中,当configCh(即PodConfig调用的Updates())返回的pod类型为ADD时,执行handler.HandlePodAdditions,在HandlePodAdditions中,处理流程如下:当pod状态为非Termination时,Kubelet遍历admitHandlers,调用Admit方法。

syncLoopIteration中除了configCh,还有其他channel(plegCh、syncCh、housekeepingCh及livenessManager)其中plegCh、syncCh及livenessManager三类channel中调用的HandlePodAddtion、HandlePodReconcile、HandlePodSyncs及HandlePodUpdates都涉及dispatch方法调用,还记得Kubelet流程中,将CPU管理器、内存管理器跟拓扑管理器封装为InternalContainerLifecycle接口,其实现Pod相关的生命周期资源管理操作,涉及CPU、内存相关的是PreStartContainer方法,其调用AddContainer方法,后续统一介绍。

5、在介绍Kubelet启动时,调用AddPodmitHandler将GetAllocateResourcesPodAdmitHandler方法加入到admitHandlers中,因此在调用Admit方法的操作,涉及到拓扑管理的也就是GetAllocateResourcesPodAdmitHandler,那么接下来就接受一下该方法。

6、在Kublet的GetAllocateResourcesPodAdmitHandler方法的处理逻辑为:当启用拓扑特性时,资源分配由拓扑管理器统一接管,如果未启用,则为cpu管理器、内存管理器及设备管理器分别管理,本文只介绍启用拓扑管理器的情况。

7、启用拓扑管理器后,Kublet的GetAllocateResourcesPodAdmitHandler返回的Admit接口类型,由拓扑管理器实现,后续统一介绍。

上述流程即为Pod大致的处理流程,下面介绍拓扑结构初始化、AddContainerAdmit方法。

1)拓扑结构初始化

拓扑结构初始化函数为

pkg/kubelet/cm/topologymanager/topology_manager.go:119
// NewManager creates a new TopologyManager based on provided policy and scopefunc NewManager(topology []cadvisorapi.Node, topologyPolicyName string, topologyScopeName string) (Manager, error) { // a. 根据cadvisor数据初始化numa信息 var numaNodes []int for _, node := range topology { numaNodes = append(numaNodes, node.Id) } // b. 判断策略为非none时,numa节点数量是否超过8,若超过,则返回错误 if topologyPolicyName != PolicyNone && len(numaNodes) > maxAllowableNUMANodes { return nil, fmt.Errorf("unsupported on machines with more than %v NUMA Nodes", maxAllowableNUMANodes) } // c. 根据传入policy名称,进行初始化policy var policy Policy switch topologyPolicyName {
case PolicyNone: policy = NewNonePolicy()
case PolicyBestEffort: policy = NewBestEffortPolicy(numaNodes)
case PolicyRestricted: policy = NewRestrictedPolicy(numaNodes)
case PolicySingleNumaNode: policy = NewSingleNumaNodePolicy(numaNodes)
default: return nil, fmt.Errorf("unknown policy: \"%s\"", topologyPolicyName) } // d. 根据传入scope名称,以初始化policy结构体初始化scope var scope Scope switch topologyScopeName {
case containerTopologyScope: scope = NewContainerScope(policy)
case podTopologyScope: scope = NewPodScope(policy)
default: return nil, fmt.Errorf("unknown scope: \"%s\"", topologyScopeName) } // e. 封装scope,返回manager结构体 manager := &manager{ scope: scope, }

a. 根据cadvisor数据初始化numa信息

b. 判断策略为非none时,numa节点数量是否超过8,若超过,则返回错误

c. 根据传入policy名称,进行初始化policy

d. 根据传入scope名称,以初始化policy结构体初始化scope

e. 封装scope,返回manager结构体

2) AddContainer        AddContainer实际调用scope的方法位于
pkg/kubelet/cm/topologymanager/scope.go:97
func (s *scope) AddContainer(pod *v1.Pod, containerID string) error { s.mutex.Lock() defer s.mutex.Unlock()
s.podMap[containerID] = string(pod.UID) return nil}        该处只做简单字典加入操作。

3)Admit

        Admit函数调用位于
pkg/kubelet/cm/topologymanager/topology_manager.go:186        其根据scope类型分别调用不同的实现:
        a、container        代码实现位于
pkg/kubelet/cm/topologymanager/scope_container.go:45
func (s *containerScope) Admit(pod *v1.Pod) lifecycle.PodAdmitResult { // Exception - Policy : none // 1. 策略为none,则跳过 if s.policy.Name() == PolicyNone { return s.admitPolicyNone(pod) } // 2. 遍历init及常规容器 for _, container := range append(pod.Spec.InitContainers, pod.Spec.Containers...) { // 2.1 计算亲和性,判断是否准入      bestHint, admit := s.calculateAffinity(pod, &container) if !admit { return topologyAffinityError() } // 2.2 记录分配结果 s.setTopologyHints(string(pod.UID), container.Name, bestHint) // 2.3 调用hint provider分配资源 err := s.allocateAlignedResources(pod, &container) if err != nil { return unexpectedAdmissionError(err) } } return admitPod()} b、pod        代码位于
pkg/kubelet/cm/topologymanager/scope_pod.go:45
func (s *podScope) Admit(pod *v1.Pod) lifecycle.PodAdmitResult { // Exception - Policy : none // 1. 策略为none,则跳过 if s.policy.Name() == PolicyNone { return s.admitPolicyNone(pod) } // 2 计算亲和性,判断是否准入   bestHint, admit := s.calculateAffinity(pod) if !admit { return topologyAffinityError() } // 3. 遍历init及常规容器 for _, container := range append(pod.Spec.InitContainers, pod.Spec.Containers...) { // 3.1 记录分配结果 s.setTopologyHints(string(pod.UID), container.Name, bestHint) // 3.2 调用hint provider分配资源 err := s.allocateAlignedResources(pod, &container) if err != nil { return unexpectedAdmissionError(err) } } return admitPod()}

        具体说明见代码注释,需要说明的是scope为container与pod的区别主要在计算亲和性,判断是否准入的阶段,同样也反应了scope与container的粒度,后续重点介绍calculateAffinity方法。

        下面zouyee带各位总结一下拓扑管理器的Admit逻辑。

        拓扑管理器为组件定义Hint Providers的接口,以发送和接收拓扑信息,CPU、memory及device都实现该接口,拓扑管理器调用AddHintPriovider加入到管理器,其中拓扑信息表示可用的 NUMA 节点和首选分配指示的位掩码。拓扑管理器策略对所提供的hint执行一组操作,并根据策略获取最优解;如果存储了与预期不符的hint,则该建议的优选字段设置为 false。所选建议可用来决定节点接受或拒绝 Pod 。之后,hint结果存储在拓扑管理器中,供Hint Providers进行资源分配决策时使用。

        对于上述两种作用域(container及pod)的calculateAffinity通用流程,汇总如下(忽略计算亲和性的差异):

        对于上图的内容,zouyee总结流程如下:

1. 遍历容器中的所有容器(scope为pod跟container的差别,上面已经说明)

2. 对于每个容器,针对容器请求的每种拓扑感知资源类型(例如gpu-vendor.com/gpu、nic-vendor.com/nic、cpu等),从一组HintProviders中获取TopologyHints。

3. 使用选定的策略,合并收集到的TopologyHints以找到最佳hint,该hint可以在所有资源类型之间对齐资源分配。

4. 循环返回hintHintProviders集合,指示他们使用合并的hint来分配他们管理的资源。

5. 如果上述步骤中的任一个失败或根据所选策略无法满足对齐要求,Kubelet将不会准入该pod。

下面zouyee根据下图依次介绍拓扑管理器涉及的结构体。

a. TopologyHints

拓扑hint对一组约束进行编码,记录可以满足给定的资源请求。目前,我们唯一考虑的约束是NUMA对齐。定义如下:

type TopologyHint struct { NUMANodeAffinity bitmask.BitMask Preferred bool}

NUMANodeAffinity字段表示可以满足资源请求的NUMA节点个数的位掩码,是bitmask类型。例如,在2个NUMA节点的系统上,可能的掩码包括:

{00}, {01}, {10}, {11}

Preferred是用来管理NUMANodeAffinity是否生效的布尔类型,如果Preferred为true那么当前的亲和度有效,如果为false那么当前的亲和度无效。使用best-effort策略时,在生成最佳hint时,优先hint将优先于非优先hint。使用restricted和single-numa-node策略时,将拒绝非优先hint。

HintProvider为每个可以满足该资源请求的NUMA节点的掩码生成一个TopologyHint。如果掩码不能满足要求,则将其省略。例如,当被要求分配2个资源时,HintProvider可能在具有2个NUMA节点的系统上提供以下hint。这些hint编码代表的两种资源可以都来自单个NUMA节点(0或1),也可以各自来自不同的NUMA节点。

{01: True}, {10: True}, {11: False}

当且仅当NUMANodeAffinity代表的信息可以满足资源请求的最小NUMA节点集时,所有HintProvider才会将Preferred字段设置为True。

{0011: True}, {0111: False}, {1011: False}, {1111: False}

如果在其他容器释放资源之前无法满足实际的首选分配,则HintProvider返回所有Preferred字段设置为False的hint列表。考虑以下场景:

1. 当前,除2个CPU外的所有CPU均已分配给容器

2. 剩余的2个CPU在不同的NUMA节点上

3. 一个新的容器请求2个CPU

在上述情况下,生成的唯一hint是{11:False}而不是{11:True}。因为可以从该系统上的同一NUMA节点分配2个CPU(虽然当前的分配状态,还不能立即分配),在可以满足最小对齐方式时,使pod进入失败并重试部署总比选择以次优对齐方式调度pod更好。

 b. HintProviders

目前,Kubernetes中仅有的HintProviders是CPUManager、MemoryManager及DeviceManager。拓扑管理器既从HintProviders收集TopologyHint,又使用合并的最佳hint调用资源分配。HintProviders实现以下接口:

type HintProvider interface { GetTopologyHints(*v1.Pod, *v1.Container) map[string][]TopologyHint Allocate(*v1.Pod, *v1.Container) error}

注意:GetTopologyHints返回一个map [string] [] TopologyHint。这使单个HintProvider可以提供多种资源类型的hint。例如,DeviceManager可以返回插件注册的多种资源类型。

当HintProvider生成hint时,仅考虑如何满足系统上当前可用资源的对齐方式。不考虑已经分配给其他容器的任何资源。

例如,考虑图1中的系统,以下两个容器请求资源:

# Container0spec: containers: - name: numa-aligned-container0 image: alpine resources: limits: cpu: 2 memory: 200Mi gpu-vendor.com/gpu: 1 nic-vendor.com/nic: 1
# Container1spec: containers: - name: numa-aligned-container1 image: alpine resources: limits: cpu: 2 memory: 200Mi gpu-vendor.com/gpu: 1 nic-vendor.com/nic: 1

如果Container0是要在系统上分配的第一个容器,则当前三种拓扑感知资源类型生成以下hint集:

cpu: {{01: True}, {10: True}, {11: False}}gpu-vendor.com/gpu: {{01: True}, {10: True}}nic-vendor.com/nic: {{01: True}, {10: True}}

已经对齐的资源分配:

{cpu: {0, 1}, gpu: 0, nic: 0}


在考虑Container1时,上述资源假定为不可用,因此将生成以下hint集:

cpu: {{01: True}, {10: True}, {11: False}}gpu-vendor.com/gpu: {{10: True}}nic-vendor.com/nic: {{10: True}}

分配的对齐资源:

{cpu: {4, 5}, gpu: 1, nic: 1}

注意:HintProviders调用Allocate的时,并未采用合并的最佳hint, 而是通过TopologyManager实现的Store接口,HintProviders通过该接口,获取生成的hint:

type Store interface { GetAffinity(podUID string, containerName string) TopologyHint}
c. Policy.Merge

每个策略都实现了合并方法,各自实现如何将所有HintProviders生成的TopologyHint集合合并到单个TopologyHint中,该TopologyHint用于提供已对齐的资源分配信息。

// 1. bestEffortfunc (p *bestEffortPolicy) Merge(providersHints []map[string][]TopologyHint) (TopologyHint, bool) { filteredProvidersHints := filterProvidersHints(providersHints) bestHint := mergeFilteredHints(p.numaNodes, filteredProvidersHints) admit := p.canAdmitPodResult(&bestHint) return bestHint, admit}// 2. restrictfunc (p *restrictedPolicy) Merge(providersHints []map[string][]TopologyHint) (TopologyHint, bool) { filteredHints := filterProvidersHints(providersHints) hint := mergeFilteredHints(p.numaNodes, filteredHints) admit := p.canAdmitPodResult(&hint) return hint, admit}// 3. sigle-numa-nodefunc (p *singleNumaNodePolicy) Merge(providersHints []map[string][]TopologyHint) (TopologyHint, bool) { filteredHints := filterProvidersHints(providersHints) // Filter to only include don't cares and hints with a single NUMA node. singleNumaHints := filterSingleNumaHints(filteredHints) bestHint := mergeFilteredHints(p.numaNodes, singleNumaHints)
defaultAffinity, _ := bitmask.NewBitMask(p.numaNodes...) if bestHint.NUMANodeAffinity.IsEqual(defaultAffinity) { bestHint = TopologyHint{nil, bestHint.Preferred} }
admit := p.canAdmitPodResult(&bestHint) return bestHint, admit}

从上述三种分配策略,可以发现Merge方法的一些类似流程:

1. filterProvidersHints

2. mergeFilteredHints

3. canAdmitPodResult

其中filterProvidersHints位于

pkg/kubelet/cm/topologymanager/policy.go:62
func filterProvidersHints(providersHints []map[string][]TopologyHint) [][]TopologyHint { // Loop through all hint providers and save an accumulated list of the // hints returned by each hint provider. If no hints are provided, assume // that provider has no preference for topology-aware allocation. var allProviderHints [][]TopologyHint for _, hints := range providersHints { // If hints is nil, insert a single, preferred any-numa hint into allProviderHints. if len(hints) == 0 { klog.Infof("[topologymanager] Hint Provider has no preference for NUMA affinity with any resource") allProviderHints = append(allProviderHints, []TopologyHint{{nil, true}}) continue }
// Otherwise, accumulate the hints for each resource type into allProviderHints. for resource := range hints { if hints[resource] == nil { klog.Infof("[topologymanager] Hint Provider has no preference for NUMA affinity with resource '%s'", resource) allProviderHints = append(allProviderHints, []TopologyHint{{nil, true}}) continue }
if len(hints[resource]) == 0 { klog.Infof("[topologymanager] Hint Provider has no possible NUMA affinities for resource '%s'", resource) allProviderHints = append(allProviderHints, []TopologyHint{{nil, false}}) continue }
allProviderHints = append(allProviderHints, hints[resource]) } } return allProviderHints}

遍历所有的HintProviders,收集并存储hint。如果HintProviders没有提供任何hint,那么就默认为该provider没有任何资源分配。最终返回allProviderHints.

其中mergeFilteredHints位于

pkg/kubelet/cm/topologymanager/policy.go:95
// Merge a TopologyHints permutation to a single hint by performing a bitwise-AND// of their affinity masks. The hint shall be preferred if all hits in the permutation// are preferred.func mergePermutation(numaNodes []int, permutation []TopologyHint) TopologyHint { // Get the NUMANodeAffinity from each hint in the permutation and see if any // of them encode unpreferred allocations. preferred := true defaultAffinity, _ := bitmask.NewBitMask(numaNodes...) var numaAffinities []bitmask.BitMask for _, hint := range permutation { // Only consider hints that have an actual NUMANodeAffinity set. if hint.NUMANodeAffinity == nil { numaAffinities = append(numaAffinities, defaultAffinity) } else { numaAffinities = append(numaAffinities, hint.NUMANodeAffinity) }
if !hint.Preferred { preferred = false } }
// Merge the affinities using a bitwise-and operation. mergedAffinity := bitmask.And(defaultAffinity, numaAffinities...) // Build a mergedHint from the merged affinity mask, indicating if an // preferred allocation was used to generate the affinity mask or not. return TopologyHint{mergedAffinity, preferred}}

func mergeFilteredHints(numaNodes []int, filteredHints [][]TopologyHint) TopologyHint { // Set the default affinity as an any-numa affinity containing the list // of NUMA Nodes available on this machine. defaultAffinity, _ := bitmask.NewBitMask(numaNodes...)
// Set the bestHint to return from this function as {nil false}. // This will only be returned if no better hint can be found when // merging hints from each hint provider. bestHint := TopologyHint{defaultAffinity, false} iterateAllProviderTopologyHints(filteredHints, func(permutation []TopologyHint) { // Get the NUMANodeAffinity from each hint in the permutation and see if any // of them encode unpreferred allocations. mergedHint := mergePermutation(numaNodes, permutation) // Only consider mergedHints that result in a NUMANodeAffinity > 0 to // replace the current bestHint. if mergedHint.NUMANodeAffinity.Count() == 0 { return }
// If the current bestHint is non-preferred and the new mergedHint is // preferred, always choose the preferred hint over the non-preferred one. if mergedHint.Preferred && !bestHint.Preferred { bestHint = mergedHint return }
// If the current bestHint is preferred and the new mergedHint is // non-preferred, never update bestHint, regardless of mergedHint's // narowness. if !mergedHint.Preferred && bestHint.Preferred { return }
// If mergedHint and bestHint has the same preference, only consider // mergedHints that have a narrower NUMANodeAffinity than the // NUMANodeAffinity in the current bestHint. if !mergedHint.NUMANodeAffinity.IsNarrowerThan(bestHint.NUMANodeAffinity) { return }
// In all other cases, update bestHint to the current mergedHint bestHint = mergedHint })
return bestHint}

mergeFilteredHints函数处理流程如下所示:

1. 通过cadvisor传递的NUMA节点数生成bitmask

2. 设置 bestHint := TopologyHint{defaultAffinity, false}如果没有符合条件的hint,返回该hint

3. 取每种资源类型生成的TopologyHints的交叉积

4. 对于交叉中的每个条目,每个TopologyHint的NUMA亲和力执行位计算。在合并hint中将此设置为NUMA亲和性。

5. 如果条目中的所有hint都将Preferred设置为True,则在合并hint中的Preferred设置为True。

6. 如果条目中存在Preferred设置为False的hint,则在合并hint中的Preferred设置为False。如果其NUMA亲和性节点数量全为0,则在合并hint中的Preferred设置为False。接上文的分配说明,Container0的hint为:

cpu: {{01: True}, {10: True}, {11: False}}gpu-vendor.com/gpu: {{01: True}, {10: True}}nic-vendor.com/nic: {{01: True}, {10: True}}

上面的算法将产生的交叉积及合并后的hint:

生成合并的hint列表之后,将根据Kubelet配置的拓扑管理器分配策略来确定哪个为最佳hint。

一般流程如下所示:

1. 根据合并hint的“狭窄度”进行排序。狭窄度定义为hint的NUMA相似性掩码中设置的位数。设置的位数越少,hint越窄。对于在NUMA关联掩码中设置了相同位数的hint,设置为最低位的hint被认为是较窄的。

2. 根据合并hint的Preferred字段排序。Preferred为true的hint优于Preferred为true的hint。

3. 为Preferred选择具有最佳设置的最窄hint。

在上面的示例中,当前支持的所有策略都将使用hint{01:True}以准入该Pod。


二、社区动向


        2.1 已知问题

       拓扑管理器所能处理的最大 NUMA 节点个数是 8。若 NUMA 节点数超过 8, 枚举可能的 NUMA 亲和性而生成hint时会导致数据爆炸式增长。

调度器不支持资源拓扑功能,当调度至该节点,但因为拓扑管理器的原因导致在该节点上调度失败。        2.2 功能特性        a. hugepage的numa应用

如前所述,当前仅可用于TopologyManager的三个HintProvider是CPUManager、MemoryManager(1.20+新增)及DeviceManager。但是,目前也正在努力增加对hugepage的支持,TopologyManager最终将能够在同一NUMA节点上分配内存,大页,CPU和PCI设备。

b. 调度

当前,TopologyManager不参与Pod调度决策,仅充当Pod Admission控制器,当调度器将Pod调度到某节点后,TopologyManager才判定应该接受还是拒绝该pod。但是可能会因为节点可用的NUMA对齐资源而拒绝pod,这跟调度系统的决定相悖。

那么我们如何解决这个问题呢?当前Kubernetes调度框架提供实现framework架构,调度算法插件化,可以实现诸如NUMA对齐之类的调度插件。

d. Pod对齐策略

如前所述,单个策略通过Kubelet命令行应用于节点上的所有Pod,而不是根据Pod进行自定义配置。

当前实现该特性最大的问题是,此功能需要更改API才能在Pod结构或其关联的RuntimeClass中表达所需的对齐策略。 





三、参考资料


1. https://kubernetes.io/blog/2020/04/01/kubernetes-1-18-feature-topoloy-manager-beta/

2.https://kubernetes.io/docs/tasks/administer-cluster/topology-manager/

3.https://kubernetes.io/docs/tasks/administer-cluster/cpu-management-policies/

4.https://github.com/kubernetes/community/blob/master/contributors/design-proposals/node/topology-manager.md


作者简介

zouyee,一般性码农,CNCF大使,CoreDNS维护者,Kubernetes、KNative社区成员。专注于基础架构,Cloud Native布道师,敏捷实践者,21年打算点亮Dapr、operator、gRPC等社区天赋。

 往期 · 精选 

1、技术分享 | Kubelet从人门到放弃:拓扑管理(上

2、干货分享 | OpenAI:Kubernetes集群近万节点的生产实践

3、干货分享 | Kubernetes将废弃PodSecurityPolicy


点个“在看”让我知道你在看


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存